4.3 - Understanding Units - The Unit Attributes
Every single entity on the battlefield—from a Marine to a mineral patch, from a Pylon to a flying Overlord—is represented in your code as a Unit
object. This is the most fundamental data structure in python-sc2
. It is both a data container and an actor: it holds all the information about an entity, and it is the object you use to issue it commands.
Mastering the Unit
object is non-negotiable. It is how you will read the battlefield, manage your base, and control your army.
A Mental Model: The Unit
as a Class
Think of every unit in the game as an instance of a class. The object holds the unit's current state and provides methods to change that state.
+------------------------------------+
| Unit (e.g., Marine) |
|====================================|
| -- Properties -- | <-- Data you can READ
| name: "Marine" |
| health: 45 |
| position: Point2(34.5, 67.5) |
| is_idle: True |
| ... |
|------------------------------------|
| -- Methods -- | <-- Actions you can CALL
| + attack(target) |
| + move(position) |
| + hold_position() |
| ... |
+------------------------------------+
How to Get a Unit
Object
You will almost never create a Unit
object manually. Instead, you will retrieve them from the game state using the helper properties on self
.
# Get a single Unit object representing your first townhall.
my_townhall = self.townhalls.first
# Get a single Unit object representing a random idle worker.
idle_worker = self.workers.idle.random_or(None)
# Loop through a collection of Unit objects.
for marine in self.units(UnitTypeId.MARINE):
# 'marine' is a Unit object.
if marine.is_idle:
print(f"Found an idle marine with tag {marine.tag}")
Key Unit
Attributes: A Developer's Reference
Once you have a Unit
object, you can inspect its attributes to make decisions.
Category | Attribute | Data Type | Use Case in a Project |
---|---|---|---|
Identity | name type_id tag | str UnitTypeId int | type_id is best for checks. Use tag to uniquely identify a unit throughout a match. |
Vitals | health , health_max shield , shield_max energy , energy_max | float | Check health to decide if a unit should retreat. Check energy to see if a caster can use an ability. |
Position | position radius | Point2 float | position is essential for all movement and distance calculations. |
Combat | weapon_cooldown is_attacking | float bool | weapon_cooldown is the key to advanced micro-management like stutter-stepping. |
State Flags | is_idle is_flying is_powered is_burrowed | bool | These booleans are the most common conditions in if statements for managing unit behavior. |
Ownership | is_yours is_enemy | bool | Crucial for distinguishing your units from the opponent's. |
A Developer's Checklist for Using a Unit
When your code gets a Unit
object, this is your thought process:
- 1. Identify It: What is this unit? (
if unit.type_id == UnitTypeId.MARINE:
) - 2. Check Its State: Is it doing what it should be? (
if unit.is_idle:
) - 3. Assess Its Condition: Is it healthy enough to fight? (
if (unit.health / unit.health_max) < 0.4:
) - 4. Give It an Order: If it's not doing the right thing, issue a command (
unit.attack(...)
).
Code Example: The Field Commander Bot
This bot demonstrates the full "Sense-Think-Act" cycle on individual Unit
objects. It finds idle Marines and sends them to attack, and pulls back any Marine that is badly damaged.
# field_commander_bot.py
from sc2.main import run_game
from sc2 import maps
from sc2.bot_ai import BotAI
from sc2.ids.unit_typeid import UnitTypeId
from sc2.player import Bot, Computer
from sc2.data import Difficulty, Race
class FieldCommanderBot(BotAI):
"""
Implements a basic Terran strategy focused on Marine production and simple combat micro.
This bot's architecture is divided by functional responsibility:
- Economy (building workers)
- Production (building structures and training units)
- Army Management (controlling military units)
"""
async def on_step(self, iteration: int):
"""
Main entry point for bot logic, executed on each game step.
Orchestrates high-level functions in a specific order.
"""
# The first action is always to make sure our workers are busy.
# 'await' is used because these functions can take time to execute.
await self.distribute_workers()
await self.manage_production()
await self.manage_army()
# =================================================================
# PRODUCTION & ECONOMY
# =================================================================
async def manage_production(self):
"""Coordinates the building of workers, structures, and units."""
await self.build_workers()
await self.build_supply_depots()
await self.build_barracks()
await self.train_marines()
async def build_workers(self):
"""Trains SCVs up to a saturation point of 16."""
# The `already_pending` check prevents queuing multiple units at once.
if (
self.townhalls.ready
and self.can_afford(UnitTypeId.SCV)
and self.supply_workers < 16
and self.already_pending(UnitTypeId.SCV) == 0
):
self.townhalls.ready.random.train(UnitTypeId.SCV)
async def build_supply_depots(self):
"""Prevents getting supply-blocked by building depots when supply is low."""
if (
self.supply_left < 5
and self.can_afford(UnitTypeId.SUPPLYDEPOT)
and self.already_pending(UnitTypeId.SUPPLYDEPOT) == 0
):
# `build` automatically finds a worker and a valid build location.
await self.build(UnitTypeId.SUPPLYDEPOT, near=self.townhalls.ready.random)
async def build_barracks(self):
"""Builds a single Barracks once a Supply Depot is complete."""
# Check prerequisite: a completed Supply Depot must exist.
if not self.structures(UnitTypeId.SUPPLYDEPOT).ready.exists:
return
# Limit to one Barracks for this simple strategy.
if (
not self.structures(UnitTypeId.BARRACKS).exists
and self.can_afford(UnitTypeId.BARRACKS)
and self.already_pending(UnitTypeId.BARRACKS) == 0
):
await self.build(UnitTypeId.BARRACKS, near=self.townhalls.ready.random)
async def train_marines(self):
"""Trains Marines from any idle Barracks."""
for barracks in self.structures(UnitTypeId.BARRACKS).ready.idle:
if self.can_afford(UnitTypeId.MARINE) and self.supply_left > 0:
barracks.train(UnitTypeId.MARINE)
# =================================================================
# ARMY MANAGEMENT
# =================================================================
async def manage_army(self):
"""Coordinates all army movements and actions."""
target = self.find_target()
await self.command_marines(target)
def find_target(self):
"""
Determines a target for the army.
Prioritizes visible enemy structures, falls back to enemy start location.
"""
if self.enemy_structures.exists:
return self.enemy_structures.random.position
return self.enemy_start_locations[0]
async def command_marines(self, target):
"""Iterates through all Marines and issues an individual command."""
for marine in self.units(UnitTypeId.MARINE):
self.command_individual_marine(marine, target)
def command_individual_marine(self, marine, target):
"""
Applies micro-management logic to a single Marine.
"""
# Retreat if health is below 50%
if marine.health / marine.health_max < 0.5:
# The 'move' command is for retreating, as units will not stop to fight.
marine.move(self.start_location)
# Attack if idle and not retreating
elif marine.is_idle:
# The 'attack' command engages any enemies encountered on the way to the target.
marine.attack(target)
# =================================================================
# LAUNCHER
# =================================================================
if __name__ == "__main__":
run_game(
maps.get("AbyssalReefLE"),
[Bot(Race.Terran, FieldCommanderBot()), Computer(Race.Zerg, Difficulty.Easy)],
realtime=True,
)